/*
 * Copyright (c) 2020 Huawei Technologies Co.,Ltd.
 *
 * openGauss is licensed under Mulan PSL v2.
 * You can use this software according to the terms and conditions of the Mulan PSL v2.
 * You may obtain a copy of Mulan PSL v2 at:
 *
 *          http://license.coscl.org.cn/MulanPSL2
 *
 * THIS SOFTWARE IS PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OF ANY KIND,
 * EITHER EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO NON-INFRINGEMENT,
 * MERCHANTABILITY OR FIT FOR A PARTICULAR PURPOSE.
 * See the Mulan PSL v2 for more details.
 * -------------------------------------------------------------------------
 *
 * security_file_enc.cpp
 *
 * IDENTIFICATION
 *	  src/gausskernel/security/keymgr/localkms/security_file_enc.cpp
 *
 * -------------------------------------------------------------------------
 */


#include "keymgr/localkms/security_file_enc.h"
#include <stdio.h>
#include <string.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <pthread.h>
#include <limits.h>
#include <openssl/rand.h>
#include <openssl/rsa.h>
#include <openssl/pem.h>
#include "securec.h"
#include "securec_check.h"
#include "keymgr/comm/security_utils.h"
#include "keymgr/encrypt/security_aead_aes_hamc_enc_key.h"
#include "keymgr/encrypt/security_encrypt_decrypt.h"

const int KEY_METERIAL_LEN = 32;
const int DRIVED_KEY_LEN = 64;
const int ITERATE_ROUD = 10000;
const int KEY_FILE_HEADER_LEN = 20;

static char g_env_value[PATH_MAX] = {0};

/* check the environment values input from the system */
static CmkemErrCode check_env_value(const char *path_value)
{
    const char danger_char_list[] = {'|', ';', '&', '$', '<', '>', '`', '\\', '\'', '\"', '{', '}', '(', ')', '[', ']',
        '~', '*', '?', '!'};

    for (size_t i = 0; i < strlen(path_value); i++) {
        for (size_t j = 0; j < sizeof(danger_char_list); j++) {
            if (path_value[i] == danger_char_list[j]) {
                cmkem_errmsg("the path '%s' contains invalid character '%c'.", path_value, path_value[i]);
                return CMKEM_CHECK_ENV_VAL_ERR;
            }
        }
    }

    return CMKEM_SUCCEED;
}

/*
 * keys generated by localkms are stored under the path ：$LOCALKMS_FILE_PATH/
 * in addition, if $LOCALKMS_FILE_PATH cannot be found, we will try to find $GAUSSHOME
 */
CmkemErrCode set_global_env_value()
{
    char *local_kms_path = NULL;
    char tmp_gausshome_buf[PATH_MAX] = {0};
    errno_t rc = 0;
    bool is_get_localkms_env = false;
    char tmp_env_val[PATH_MAX] = {0};
    CmkemErrCode ret = CMKEM_SUCCEED;

    if (strlen(g_env_value) > 0) {
        return CMKEM_SUCCEED;
    }

    local_kms_path = km_env_get("LOCALKMS_FILE_PATH");
    if (local_kms_path == NULL || realpath(local_kms_path, tmp_env_val) == NULL) {
        /* fail to get LOCALKMS_FILE_PATH, then try to get GAUSSHOME */
        local_kms_path = km_env_get("GAUSSHOME");
        if (local_kms_path != NULL) {
            rc = sprintf_s(tmp_gausshome_buf, sizeof(tmp_gausshome_buf), "%s/%s", local_kms_path, "/etc/localkms");
            securec_check_ss_c(rc, "", "");

            /* judge whether the $GAUSSHOME is obtained or not */
            if (realpath(tmp_gausshome_buf, tmp_env_val) != NULL) {
                is_get_localkms_env = true;
            }
        }
    } else {
        is_get_localkms_env = true;
    }

    if (!is_get_localkms_env) {
        cmkem_errmsg("failed to get the environment value : '%s' or the path : '%s'.", "$LOCALKMS_FILE_PATH",
            "$GAUSSHOME/etc/localkms/");
        return CMKEM_GET_ENV_VAL_ERR;
    }

    ret = check_env_value(tmp_env_val);
    check_cmkem_ret(ret);

    rc = strcpy_s(g_env_value, PATH_MAX, tmp_env_val);
    securec_check_c(rc, "", "");
    
    return CMKEM_SUCCEED;
}

/* before creating or reading cmk and rand file, we will check if the files already exist */
CmkemErrCode check_file_exist(const char *real_file_path, CheckKeyFileType chk_type)
{
    bool is_file_exist = false;
    struct stat statbuf;

    is_file_exist = (lstat(real_file_path, &statbuf) < 0) ? false : true;
    if (chk_type == CREATE_KEY_FILE && is_file_exist) {
        cmkem_errmsg("cannot create file, the file '%s' already exists.\n", real_file_path);
        return CMKEM_CREATE_FILE_ERR;
    } else if (chk_type == READ_KEY_FILE && !is_file_exist) {
        cmkem_errmsg("cannot read file, failed to find file '%s'.\n", real_file_path);
        return CMKEM_FIND_FILE_ERR;
    }

    return CMKEM_SUCCEED;
}

/*
 * if para:is_write_header == true, we will create a file header, and write the size of conent to header
 * after that, if trying to read all content from file, we will read header to calcule the size of content
 * buffer before reading all content.
 *
 * although it's redundant to add a header, in order to keep the compatibility with the old version, we want
 * to keep using this way
 */
static CmkemErrCode create_file_and_write(const char *real_path, const unsigned char *content, size_t content_len,
    bool is_write_header)
{
    int fd = 0;
    char head[KEY_FILE_HEADER_LEN] = {0};
    errno_t rc = 0;
    ssize_t written = 0;

    fd = open(real_path, O_CREAT | O_RDWR, S_IRUSR | S_IWUSR);
    if (fd == -1) {
        cmkem_errmsg("failed to create file '%s'.\n", real_path);
        return CMKEM_CREATE_FILE_ERR;
    }

    if (is_write_header) {
        rc = sprintf_s(head, sizeof(head), "%lu", content_len);
        securec_check_ss_c(rc, "", "");
        written = write(fd, head, sizeof(head));
        if (written != sizeof(head)) {
            cmkem_errmsg("failed to write header to file '%s'.\n", real_path);
            (void)close(fd);
            return CMKEM_WRITE_FILE_ERR;
        }
    }
    
    written = write(fd, content, content_len);
    if (written != content_len) {
        cmkem_errmsg("failed to write content to file '%s'.\n", real_path);
        (void)close(fd);
        return CMKEM_WRITE_FILE_ERR;
    }

    (void)close(fd);
    return CMKEM_SUCCEED;
}

/*
 * in SQL :
 *      CREATE CLIENT MASTER KEY xxx (KEY_STORE = localkms, KEY_PATH = $key_path_value, ...)
 * for each $key_path_value, we will create 4 file:
 *      1. $LOCALKMS_FILE_PATH/$key_path_value.pub ： store public key cipher of asymmetric key
 *      2. $LOCALKMS_FILE_PATH/$key_path_value.pub.rand ： store rands that are used to derive a key to encrypt
 *          public key plain
 *      3. $LOCALKMS_FILE_PATH/$key_path_value.priv ： store private key cipher of asymmetric key
 *      2. $LOCALKMS_FILE_PATH/$key_path_value.priv.rand ： store rands that are used to derive a key to encrypt
 *          private key plain
 */
void get_file_path_from_cmk_id(const char *cmk_id_str, LocalkmsFileType file_type, char *file_path_buf, size_t buf_len)
{
    error_t rc = 0;
    const char *file_extension = NULL;
    
    switch (file_type) {
        case PUB_KEY_FILE:
            file_extension = ".pub";
            break;
        case PRIV_KEY_FILE:
            file_extension = ".priv";
            break;
        case PUB_KEY_ENC_IV_FILE:
            file_extension = ".pub.rand";
            break;
        case PRIV_KEY_ENC_IV_FILE:
            file_extension = ".priv.rand";
            break;
        default:
            break;
    }

    rc = sprintf_s(file_path_buf, buf_len, "%s/%s%s", g_env_value, cmk_id_str, file_extension);
    securec_check_ss_c(rc, "", "");
}

CmkemErrCode encrypt_and_write_key(const char *key_file_path, CmkemUStr *key_plain)
{
    unsigned char iv[KEY_METERIAL_LEN] = {0};
    unsigned char salt[KEY_METERIAL_LEN] = {0};
    unsigned char iv_salt_buf[sizeof(iv) + sizeof(salt)] = {0};
    unsigned char derived_key[DRIVED_KEY_LEN] = {0};
    unsigned char tmp_cipher[RSA3072_KEN_LEN] = {0};
    int tmp_cipher_len = 0;
    char rand_file_path[PATH_MAX] = {0};
    errno_t rc = 0;
    CmkemErrCode ret = CMKEM_SUCCEED;

    if (RAND_bytes(iv, sizeof(iv)) != 1 || RAND_bytes(salt, sizeof(salt)) != 1) {
        return CMKEM_DERIVED_KEY_ERR;
    }

    if (PKCS5_PBKDF2_HMAC((const char *)iv, sizeof(iv), salt, sizeof(salt), ITERATE_ROUD, EVP_sha256(),
        sizeof(derived_key), derived_key) != 1) {
        return CMKEM_DERIVED_KEY_ERR;
    }

    AeadAesHamcEncKey derived_aead_key = AeadAesHamcEncKey(derived_key, DRIVED_KEY_LEN);
    tmp_cipher_len = encrypt_data(key_plain->ustr_val, key_plain->ustr_len, derived_aead_key,
        EncryptionType::DETERMINISTIC_TYPE, tmp_cipher, AEAD_AES_256_CBC_HMAC_SHA256);
    if (tmp_cipher_len <= 0) {
        return CMKEM_ENC_CMK_ERR;
    }

    ret = create_file_and_write(key_file_path, tmp_cipher, (size_t)tmp_cipher_len, true);
    if (ret != CMKEM_SUCCEED) {
        return ret;
    }

    rc = sprintf_s(rand_file_path, PATH_MAX, "%s.rand", key_file_path);
    securec_check_ss_c(rc, "", "");

    for (size_t i = 0; i < sizeof(iv); i++) {
        iv_salt_buf[i] = iv[i];
    }
    for (size_t i = 0; i < sizeof(salt); i++) {
        iv_salt_buf[sizeof(iv) + i] = salt[i];
    }

    return create_file_and_write(rand_file_path, iv_salt_buf, sizeof(iv_salt_buf), true);
}

/* the input file must contain a header which store the size value of file content */
static CmkemErrCode read_content_from_file(const char *real_path, unsigned char *buf, const size_t buf_len,
    size_t *content_len)
{
    int fd = 0;
    char header[KEY_FILE_HEADER_LEN] = {0};

    fd = open(real_path, O_RDONLY, 0);
    if (fd < 0) {
        cmkem_errmsg("failed to open file '%s'.\n", real_path);
        return CMKEM_OPEN_FILE_ERR;
    }

    if (read(fd, header, sizeof(header)) < 0) {
        cmkem_errmsg("failed to read file '%s'.\n", real_path);
        close(fd);
        return CMKEM_READ_FILE_ERR;
    }
    *content_len = atoi(header);

    if (*content_len > buf_len) {
        cmkem_errmsg("the header of file '%s' is invalid.\n", real_path);
        close(fd);
        return CMKEM_READ_FILE_ERR;
    }

    if (read(fd, buf, *content_len) < 0) {
        cmkem_errmsg("failed to read from file '%s'.\n", real_path);
        close(fd);
        return CMKEM_READ_FILE_ERR;
    }

    close(fd);
    return CMKEM_SUCCEED;
}

CmkemErrCode read_iv_and_salt(const char *rand_path, unsigned char *iv, size_t iv_len, unsigned char *salt,
    size_t salt_len)
{
    unsigned char iv_salt_buf[iv_len + salt_len] = {0};
    size_t iv_salt_len = 0;
    CmkemErrCode ret = CMKEM_SUCCEED;

    ret = read_content_from_file(rand_path, iv_salt_buf, sizeof(iv_salt_buf), &iv_salt_len);
    if (ret != CMKEM_SUCCEED) {
        return ret;
    } else if (iv_salt_len < sizeof(iv_salt_buf)) {
        return CMKEM_READ_FILE_ERR;
    }

    for (size_t i = 0; i < iv_len; i++) {
        iv[i] = iv_salt_buf[i];
    }
    for (size_t i = 0; i < salt_len; i++) {
        salt[i] = iv_salt_buf[iv_len + i];
    }

    return CMKEM_SUCCEED;
}

/*
 * cmk plain is encrypted byfore stored.
 * the iv_and_salt are used to derive a key to encrypt cmk plain, as well as decrypt cmk cipher
 */
static CmkemErrCode read_rand_and_drive_key(const char *rand_path, CmkemUStr **drived_key)
{
    unsigned char iv[KEY_METERIAL_LEN] = {0};
    unsigned char salt[KEY_METERIAL_LEN] = {0};
    CmkemUStr *reg_drived_key = NULL;
    CmkemErrCode ret = CMKEM_SUCCEED;

    ret = read_iv_and_salt(rand_path, iv, sizeof(iv), salt, sizeof(salt));
    if (ret != CMKEM_SUCCEED) {
        return ret;
    }

    reg_drived_key = malloc_cmkem_ustr(DRIVED_KEY_LEN);
    if (reg_drived_key == NULL) {
        return CMKEM_MALLOC_MEM_ERR;
    }

    if (PKCS5_PBKDF2_HMAC((const char *)iv, sizeof(iv), salt, sizeof(salt), ITERATE_ROUD, EVP_sha256(),
        DRIVED_KEY_LEN,  reg_drived_key->ustr_val) != 1) {
        free_cmkem_ustr(reg_drived_key);
        return CMKEM_DERIVED_KEY_ERR;
    }

    *drived_key = reg_drived_key;
    return CMKEM_SUCCEED;
}

/*
 * cmk is stored as cihper, so, rather then read it directly, we will :
 *      1. read rand file firstly (rand_file_path = cmk_cipher_file_path.rand)
 *      2. derive a key to decrypt cmk cipher
 *      3. now, we finally get cmk plain
 */
static CmkemErrCode read_cmk_plain(const char *real_cmk_path, unsigned char *cmk_plain, size_t *plain_len)
{
    char rand_file_path[PATH_MAX] = {0};
    CmkemUStr *derivied_key = NULL;
    unsigned char cmk_cipher[RSA3072_KEN_LEN] = {0};
    size_t cmk_cipher_len = 0;
    int tmp_plainlen = 0;
    CmkemErrCode ret = CMKEM_SUCCEED;
    errno_t rc = 0;

    rc = sprintf_s(rand_file_path, PATH_MAX, "%s.rand", real_cmk_path);
    securec_check_ss_c(rc, "", "");

    ret = read_rand_and_drive_key(rand_file_path, &derivied_key);
    if (ret != CMKEM_SUCCEED) {
        free_cmkem_ustr(derivied_key);
        return ret;
    }

    ret = read_content_from_file(real_cmk_path, cmk_cipher, sizeof(cmk_cipher), &cmk_cipher_len);
    if (ret != CMKEM_SUCCEED) {
        free_cmkem_ustr(derivied_key);
        return ret;
    }

    AeadAesHamcEncKey derived_aead_key = AeadAesHamcEncKey(derivied_key->ustr_val, DRIVED_KEY_LEN);
    free_cmkem_ustr(derivied_key);
    tmp_plainlen = decrypt_data(cmk_cipher, cmk_cipher_len, derived_aead_key, cmk_plain, AEAD_AES_256_CBC_HMAC_SHA256);
    if (tmp_plainlen <= 0) {
        return CMKEM_DEC_CMK_ERR;
    }
    *plain_len = (size_t)tmp_plainlen;

    return CMKEM_SUCCEED;
}

CmkemErrCode read_and_decrypt_cmk(const char *key_path, AsymmetricKeyType key_type, CmkemUStr **key_palin)
{
    CmkemErrCode ret = CMKEM_SUCCEED;
    char key_file_path[PATH_MAX] = {0};
    CmkemUStr *ret_key_plain = NULL;

    if (key_type == PUBLIC_KEY) {
        get_file_path_from_cmk_id(key_path, PUB_KEY_FILE, key_file_path, PATH_MAX);
    } else {
        get_file_path_from_cmk_id(key_path, PRIV_KEY_FILE, key_file_path, PATH_MAX);
    }
    
    ret_key_plain = malloc_cmkem_ustr(MAX_ASYMM_KEY_BUF_LEN);
    if (ret_key_plain == NULL) {
        return CMKEM_MALLOC_MEM_ERR;
    }

    ret = read_cmk_plain(key_file_path, ret_key_plain->ustr_val, &ret_key_plain->ustr_len);
    if (ret != CMKEM_SUCCEED) {
        free_cmkem_ustr_with_erase(ret_key_plain);
        return ret;
    }

    *key_palin = ret_key_plain;
    return CMKEM_SUCCEED;
}